#region License
// Product:
// MCM (Message-Command-Message)
// mcm-framework.net
// 
// Author:
// Pawel Idzikowski
// Cologne, Germany
// idzikowski@polenter.com
// 
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the author(s) be held liable for any damages arising from
// the use of this software.
// 
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 
//     1. The origin of this software must not be misrepresented; you must not
//       claim that you wrote the original software. If you use this software
//       in a product, an acknowledgment in the product documentation would be
//       appreciated but is not required.
// 
//     2. Altered source versions must be plainly marked as such, and must not
//       be misrepresented as being the original software.
// 
//     3. This notice may not be removed or altered from any source distribution.
// 
#endregion
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;

namespace Polenter.Mcm
{
    /// <summary>
    /// Invokes the subscriber method and manages subscriber components
    /// </summary>
    internal class MessageBroadcast
    {
        private readonly MessageInvokerGroupCollection _invokerGroups = new MessageInvokerGroupCollection();

        /// <summary>
        /// Contains SynchronizationContext in which the class was creatd
        /// </summary>
        private readonly SynchronizationContext _uiContext;

        /// <summary>
        /// Should be created in the UI thread (if any)
        /// </summary>
        public MessageBroadcast()
        {
            _uiContext = SynchronizationContext.Current;
        }

        /// <summary>
        /// Broadcast message to the channel
        /// </summary>
        /// <param name="message"></param>
        /// <param name="channelName"></param>
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Broadcast(object message, string channelName)
        {
            if (_invokerGroups.Count == 0)
            {
                return;
            }
            if (!_invokerGroups.Contains(channelName))
            {
                return;
            }

            MessageInvokerGroup invokerGroup = _invokerGroups[channelName];

            IEnumerable<MessageInvoker> validInvokers = getInvokersForMessageType(message,
                                                                                  invokerGroup.Invokers);
            foreach (MessageInvoker messageInvoker in validInvokers)
            {
                if (_uiContext == null)
                {
                    // No UI context, no control over the synchronization
                    if (messageInvoker.UiThreadSynchronizationMode != UIThreadSynchronizationMode.IgnoreSynchronization)
                    {
                        var mes = new StringBuilder();
                        mes.AppendLine(
                            string.Format(
                                "The method {0}.{1} should be invoked in the UI Thread but the SynchronizationContext for the UI thread is not defined.",
                                messageInvoker.InvokerObject.GetType().Name, messageInvoker.MethodInfo.Name));
                        mes.AppendLine(
                            "This could happen if you create your custom ComponentContainer too early. The recommended aproach is creating the custom ComponentContainer in the constructor of the main form or later.");
                        throw new InvalidOperationException(mes.ToString());
                    }

                    invokeMethodInfo(message, messageInvoker);
                    continue;
                }
                switch (messageInvoker.UiThreadSynchronizationMode)
                {
                    case UIThreadSynchronizationMode.SendSynchronousInUIThread:
                        _uiContext.Send(invokeMethodInfo, new InvokingArgument(message, messageInvoker));
                        break;
                    case UIThreadSynchronizationMode.PostAsynchronousInUIThread:
                        _uiContext.Post(invokeMethodInfo, new InvokingArgument(message, messageInvoker));
                        break;
                    default:
                        invokeMethodInfo(message, messageInvoker);
                        break;
                }
            }
        }

        private static void invokeMethodInfo(object state)
        {
            var arg = (InvokingArgument) state;
            invokeMethodInfo(arg.Message, arg.MessageInvoker);
        }

        private static void invokeMethodInfo(object message, MessageInvoker messageInvoker)
        {
            messageInvoker.MethodInfo.Invoke(messageInvoker.InvokerObject, new[] {message});
        }

        private static IEnumerable<MessageInvoker> getInvokersForMessageType(object message,
                                                                             IEnumerable<MessageInvoker> invokers)
        {
            Type messageType = message.GetType();
            foreach (MessageInvoker messageInvoker in invokers)
            {
                if (messageInvoker.MessageType == messageType || messageInvoker.MessageType.IsInstanceOfType(message))
                {
                    yield return messageInvoker;
                }
            }
        }

        /// <summary>
        /// Subscribes methods to the message channel
        /// </summary>
        /// <param name="component"></param>
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void RegisterComponent(object component)
        {
            MethodInfo[] methods =
                component.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic |
                                               BindingFlags.DeclaredOnly);
            foreach (MethodInfo methodInfo in methods)
            {
                object[] customAttributes = methodInfo.GetCustomAttributes(typeof (MessageSubscriberAttribute), false);
                if (customAttributes.Length == 0)
                {
                    continue;
                }
                var messageSubscriberAttribute = customAttributes[0] as MessageSubscriberAttribute;
                if (messageSubscriberAttribute == null)
                {
                    continue;
                }

                MessageInvokerGroup invokerGroup = getOrCreateInvokerGroup(messageSubscriberAttribute.MessageChannelName);

                ParameterInfo[] parameters = methodInfo.GetParameters();
                ParameterInfo firstParameter = parameters[0];


                // Add invoker
                var invoker = new MessageInvoker();
                invoker.InvokerObject = component;
                invoker.MethodInfo = methodInfo;
                invoker.MessageType = firstParameter.ParameterType;
                invoker.UiThreadSynchronizationMode = messageSubscriberAttribute.UIThreadSynchronizationMode;
                invokerGroup.Invokers.Add(invoker);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void UnregisterComponent(object component)
        {
            foreach (MessageInvokerGroup invokerGroup in _invokerGroups)
            {
                var invokersToRemove = new List<MessageInvoker>();
                foreach (MessageInvoker invoker in invokerGroup.Invokers)
                {
                    if (invoker.InvokerObject.Equals(component))
                    {
                        invokersToRemove.Add(invoker);
                    }
                }

                foreach (MessageInvoker invoker in invokersToRemove)
                {
                    invokerGroup.Invokers.Remove(invoker);
                }
            }
        }

        /// <summary>
        /// Clears invoker groups
        /// </summary>
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Clear()
        {
            _invokerGroups.Clear();
        }

        private MessageInvokerGroup getOrCreateInvokerGroup(string messageChannelName)
        {
            if (_invokerGroups.Contains(messageChannelName))
            {
                return _invokerGroups[messageChannelName];
            }
            var item = new MessageInvokerGroup(messageChannelName);
            _invokerGroups.Add(item);
            return item;
        }

        #region Nested type: InvokingArgument

        /// <summary>
        /// Informs how the method should be invoked
        /// </summary>
        private class InvokingArgument
        {
            public InvokingArgument(object message, MessageInvoker messageInvoker)
            {
                Message = message;
                MessageInvoker = messageInvoker;
            }

            public object Message { get; private set; }
            public MessageInvoker MessageInvoker { get; private set; }
        }

        #endregion
    }
}